/*
 * Copyright (C) 2009 Anatoly Zenkov
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the author be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 */
package com.kartoshka.core.output {
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.system.Capabilities;
	import flash.system.System;
	import flash.text.Font;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.utils.Dictionary;

	/**
	 * @author AnatolyZenkov
	 */
	public class Console extends Sprite {
		/////////////////////////////////////////////////////////////////
		//
		// STATICS
		//
		/////////////////////////////////////////////////////////////////
		// STATIC CONSTS
		private static const T : int = 1;
		private static const B : int = 2;
		private static const L : int = 4;
		private static const R : int = 8;
		private static const ONE_DIV_3 : Number = 0.33333333333333333333333;
		private static const TWO_DIV_3 : Number = 0.66666666666666666666666;
		// STATIC VARS
		private static var _dispatcher : EventDispatcher;
		private static var _commands : Dictionary;
		private static var _colors : Dictionary;
		private static var _outputs : Array;
		private static var _isRelease : Boolean;
		private static var _framePrefix : String;
		private static var _frames : uint;
		private static var _wrap : Boolean;
		private static var _log : String;
		private static var _instance : Console = new Console();
		private static var _firstReport : Boolean;
		private static var _stage : Stage;
		/**
		 * Allows additionally to trace sended messages to IDE console.
		 */
		public static var traceMessages : Boolean = true;

		/////////////////////////////////////////////////////////////////
		// PUBLICS
		/////////////////////////////////////////////////////////////////
		/**
		 * Outputs common report message to console. Ignores in release build of project when <code>debugOnly</code> is true.
		 **/
		public static function reportMessage(text : Object, debugOnly : Boolean = false) : void {
			if (_isRelease && debugOnly) return;
			updateInstances("r", text, _framePrefix);
		}

		/**
		 * Outputs warning message to console (dark yellow). Ignores in release build of project when <code>debugOnly</code> is true.
		 */
		public static function warningMessage(text : Object, debugOnly : Boolean = false) : void {
			if (_isRelease && debugOnly) return;
			updateInstances("w", text, _framePrefix);
		}

		/**
		 * Outputs erorr message to console (red). Ignores in release build of project when <code>debugOnly</code> is true.
		 */
		public static function errorMessage(text : Object, debugOnly : Boolean = false) : void {
			if (_isRelease && debugOnly) return;
			updateInstances("e", text, _framePrefix);
		}

		/**
		 * Binds custom command. Function <code>func</code> invokes when user types <code>command</code> in input text field
		 * of console and press "Enter" key. Your <code>func</code> must have a string as single input parameter.
		 */
		public static function bindCommand(command : String, func : Function, description : String = "") : void {			
			_commands[command] = {func:func, desc:description};
		}

		/**
		 * Unbinds custom command.
		 */
		public static function unbindCommand(command : String) : void {
			_commands[command] = null;
		}

		/**
		 * Call execution of command that was predefined or previously binded by <code>bindCommand()</code> function.
		 * If there is any space symbol in command line, everything before it treats as command name and everything after as command parameter.
		 * This last substring passes to function which binded to command name as String object. 
		 */
		public static function execute(command : String) : void {
			var a : Array = command.split(" ");
			var c : String = a[0];
			var p : String = a.length == 1 ? null : (a.splice(1) as Array).join(" ");
			updateInstances("r", command);
			if (!_commands[c]) {
				updateInstances("e", "Unknown command. Type \"help\" for avaliable command list.");
				return;
			}
			try {
				p ? (_commands[c]["func"] as Function)(p) : (_commands[c]["func"] as Function)();
			} catch (e : Error) {
				updateInstances("e", e.message);
				updateInstances("e", e.getStackTrace());
				return;
			}
		}

		/////////////////////////////////////////////////////////////////
		// PRIVATES
		/////////////////////////////////////////////////////////////////
		private static function updateInstances(nodeName : String, text : Object, prefix : String = "") : void {
			var xml : XML = new XML("<textformat size=\"10\" font=\"_typewriter\">(" + prefix + text + "\n)</textformat>");
			xml.@color = _colors[nodeName];
			_outputs[_outputs.length] = xml.toXMLString();
			_dispatcher.dispatchEvent(new Event("u"));
			_log += prefix + text + "\n";
			if (traceMessages) trace(prefix + text);
		}

		private static function isRelease() : Boolean {
			if ( !( new Error().getStackTrace() ) ) return true;
			return ( new Error().getStackTrace().split("\n")[ 1 ] as String ).replace(/.+\/.+\(\)/, "") == "";
		}

		/////////////////////////////////////////////////////////////////
		// COMMANDS
		/////////////////////////////////////////////////////////////////
		private static function commandListReport() : void {
			var systemReport : Array = [];
			for (var c:String in _commands) systemReport[systemReport.length] = c + " — " + _commands[c]["desc"];
			systemReport.sort();
			var s : String;
			for each (s in systemReport) updateInstances("s", s);
		}

		private static function fontListReport() : void {
			var fonts : Array = Font.enumerateFonts();
			if (fonts.length) {
				updateInstances("s", "Embeded fonts:");
				for each (var f : Font in fonts) updateInstances("s", f.fontName + "\t\t" + f.fontStyle);
				updateInstances("s", "Total fonts: " + fonts.length);
			} else {
				updateInstances("s", "No fonts currently embedded.");
			}
		}

		private static function fullSystemReport() : void {
			var systemReport : Array = unescape(Capabilities.serverString).split("&");
			var s : String;
			for each (s in systemReport) updateInstances("s", s);
		}

		private static function clearOutput() : void {
			_outputs = [];
			_log = "";
			_dispatcher.dispatchEvent(new Event("u"));
		}

		private static function wordWrapOutput() : void {
			_wrap = !_wrap;
			_dispatcher.dispatchEvent(new Event("w"));
		}

		private static function copyOutput() : void {
			System.setClipboard(_log);
			updateInstances("s", "Copied to clipboard.");
		}

		private static function systemReport() : void {
			_stage.dispatchEvent(new Event("frameworkInfoRequest"));
			updateInstances("s", "OS: " + Capabilities.os);
			updateInstances("s", "Screen resolution: " + Capabilities.screenResolutionX + "x" + Capabilities.screenResolutionY);
			updateInstances("s", "Player version: " + Capabilities.version + (Capabilities.isDebugger ? " (debug)" : ""));
			updateInstances("s", "Player type: " + Capabilities.playerType);
			updateInstances("s", "SWF compilation: " + (Capabilities.isDebugger ? (_isRelease ? "Release" : "Debug") : "N/A"));
			updateInstances("s", "SWF URL: " + _stage.loaderInfo.url);
			updateInstances("s", "SWF size (uncompressed): " + (_stage.loaderInfo.bytesTotal / 1024) + "Kb");
		}

		private static function flashVarsReport() : void {
			var params : Array = [];
			for (var prop:String in _stage.loaderInfo.parameters) {
				params[params.length] = prop + ":\t" + _stage.loaderInfo.parameters[prop];
			}
			if (params.length) {
				updateInstances("s", "Recieved FlashVars parameters:");
				var s : String;
				for each (s in params) updateInstances("s", s);
				updateInstances("s", "Total parameters: " + params.length);
			} else {
				updateInstances("s", "FlashVars parameters not recieved.");
			}
		}

		private static function frameworkReport(event : Event) : void {
			updateInstances("w", "Framework info not processing yet.");
		}

		/////////////////////////////////////////////////////////////////
		//
		// INSTANCE
		//
		/////////////////////////////////////////////////////////////////
		/////////////////////////////////////////////////////////////////
		// PRIVATES
		/////////////////////////////////////////////////////////////////
		private var _relatedX : Number;
		private var _relatedY : Number;
		private var _relatedWidth : Number;
		private var _relatedHight : Number;
		private var _validateNextTime : Boolean;
		private var _buttons : Array;
		private var _output : TextField;
		private var _input : TextField;
		private var _alignID : uint;
		private var _inputs : Array;
		private var _currentInput : uint = 0;
		private var _scrollV : ConsoleScroll;

		/**
		 * Useful developer tool. Allows to monitor application workflow even in nondebug player.
		 * You can send messages of different types from any place of your project to it. There are three types of messages:
		 * report, warning and error. Each one is colored in different color in console.
		 * Also it is possible to execute a functions with parameters from console's input text field.
		 * There are some usful build functions (capabilities report, embedded fonts, copy log to clipboard)
		 * and also the binding of new commands to custom functions is avaliable. Has simple interface that allows to snap
		 * console body to different corners of the stage. 
		 */
		public function Console() {
			if (!_instance) {
				_dispatcher = new EventDispatcher();
				_commands = new Dictionary();
				_colors = new Dictionary();
				_colors["r"] = 0;
				_colors["w"] = 0x999900;
				_colors["e"] = 0xCC0000;
				_colors["s"] = 0x7C8588;
				_colors["g"] = 0x008800;
				_outputs = [];
				_isRelease = isRelease();
				_framePrefix = "f0000000\t";
				_frames = 0;
				_wrap = false;
				_firstReport = true;
				_log = "";
				addEventListener(Event.ENTER_FRAME, frameCount_enterFrameHandler);
				bindCommand("help", commandListReport, "Available command list.");
				bindCommand("fonts", fontListReport, "Embedded font list.");
				bindCommand("system", fullSystemReport, "Full system capabilities report.");
				bindCommand("clear", clearOutput, "Clear console.");
				bindCommand("copy", copyOutput, "Copy output to clipboard.");
				bindCommand("wrap", wordWrapOutput, "Switch console word wrap.");
				return;
			}
			
			_scrollV = new ConsoleScroll("_scrlH");
			tabChildren = false;
			addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
			_output = new TextField();
			_scrollV.y = _output.x = _output.y = ConsoleButton.BORDER;
			_output.multiline = _output.border = true;
			_output.wordWrap = _wrap;
			addChild(_output);
			
			_input = new TextField();
			_input.x = ConsoleButton.BORDER;
			_input.defaultTextFormat = new TextFormat("_typewriter", 10);
			_input.border = true;
			_output.borderColor = _input.borderColor = _colors["s"];
			_input.type = "input";
			_input.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);
			_input.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler);
			_input.height = ConsoleButton.BORDER;
			addChild(_input);
			_inputs = [];
			
			addChild(_scrollV);
			
			_dispatcher.addEventListener("u", updateHandler);
			_dispatcher.addEventListener("w", wrapHandler);
			
			_buttons = [];
			var b : ConsoleButton;
			var rotation : Number = -45;
			var bID : Array = [T, T | L, L, L | B, B, B | R, R, T | R];
			var bX : Array = [.5,	0,	0,	0,	.5,	1,	1,	1];
			var bY : Array = [0, 	0,	.5,	1,	1,	1,	.5,	0];
			for (var i : int = 0;i < 8; i++) {
				b = new ConsoleButton();
				b.arrowRotation = rotation -= 45;
				b.id = bID[i];
				b.relatedX = bX[i];
				b.relatedY = bY[i];
				b.addEventListener(MouseEvent.CLICK, buttonClickHandler);
				_buttons[_buttons.length] = b;
				addChild(b);
			}
			
			align = B;
		}

		/////////////////////////////////////////////////////////////////
		//  HANDLERS
		/////////////////////////////////////////////////////////////////
		private function addedToStageHandler(event : Event) : void {
			if (event.target != this) return;
			if (_validateNextTime) {
				stage.addEventListener(Event.RENDER, renderHandler);
				stage.invalidate();
				_validateNextTime = false;
			}
			addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
			addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
			addEventListener(MouseEvent.MOUSE_DOWN, stopIt);
			addEventListener(MouseEvent.CLICK, stopIt);
			stage.addEventListener(Event.RESIZE, resizeHandler);
			stage.addEventListener("_scrlH", scrollHandler, true);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, stage_keyDownHandler);
			parent.addEventListener(Event.ADDED, parent_addedHandler);
			
			if (!_firstReport) return;
			_firstReport = false;
			_stage = stage;
			_stage.addEventListener("frameworkInfoPass", frameworkReport);
			bindCommand("sys", systemReport, "System report.");
			bindCommand("flashvars", flashVarsReport, "Received FlashVars parameters and values list. ");
			systemReport();
			flashVarsReport();
			updateInstances("s", "=======================================");
		}

		private function parent_addedHandler(event : Event) : void {
			var n:int = parent.getChildIndex(this);
			while(n < parent.numChildren-1) {
				parent.swapChildrenAt(n, n+1);
				n = parent.getChildIndex(this);
			}
		}

		private function keyUpHandler(event : KeyboardEvent) : void {
			if (event.keyCode == 40 || event.keyCode == 38) {
				_input.setSelection(_input.text.length, _input.text.length);
				return;
			}
		}

		private function keyDownHandler(event : KeyboardEvent) : void {
			if (_inputs.length) {
				if (event.keyCode == 40 || event.keyCode == 38) {
					_input.text = _inputs[_currentInput];
					_currentInput = event.keyCode == 38 ? Math.max(0, _currentInput - 1) : Math.min(_inputs.length - 1, _currentInput + 1);
					return;
				}
			}
			if (event.keyCode != 13 || !_input.text.length) return;
			var s : String = _input.text.replace(/\s*/, "");
			_inputs[_inputs.length] = _input.text;
			_currentInput = _inputs.length - 1;
			_input.text = "";
			execute(s);
		}

		private function updateHandler(event : Event) : void {
			if (!_outputs.length) {
				_output.text = "";
				return;
			}
			var s : String = "<flashrichtext version=\"1\">" + _outputs[_outputs.length - 1] + "</flashrichtext>";
			_output.insertXMLText(_output.length, _output.length, s, false);
			checkScroll();
			dispatchEvent(new SEvnt("_scrlH", 1));
		}

		private function wrapHandler(event : Event) : void {
			_output.wordWrap = _wrap;
			checkScroll();
			dispatchEvent(new SEvnt("_scrlH", 1));
		}

		private function buttonClickHandler(event : MouseEvent) : void {
			align = event.target["id"];
		}

		public function set align(a : uint) : void {
			_alignID = a;
			var tb : Boolean = (a & B) == B || (a & T) == T;
			var lr : Boolean = (a & L) == L || (a & R) == R;
			if (tb) {
				_relatedY = (a & T) == T ? 0 : TWO_DIV_3;
				_relatedWidth = lr ? .5 : 1;
			} else {
				_relatedY = 0;
				_relatedWidth = ONE_DIV_3;
				_relatedHight = 1;
			}
			if (lr) {
				_relatedX = (a & L) == L ? 0 : tb ? .5 : TWO_DIV_3;
				_relatedHight = tb ? ONE_DIV_3 : 1;
			} else {
				_relatedX = 0;
				_relatedWidth = 1;
				_relatedHight = ONE_DIV_3;
			}
			invalidate();
		}

		private function stage_keyDownHandler(event : KeyboardEvent) : void {
			if (!event.shiftKey) return;
			if (!event.ctrlKey) return;
			if (event.keyCode != 3) return;
			visible = !visible;
		}

		private function scrollHandler(event : SEvnt) : void {
			_output.scrollV = _output.maxScrollV * event.n;
		}

		private function stopIt(event : MouseEvent) : void {
			event.stopPropagation();
		}

		private function rollOutHandler(event : MouseEvent) : void {
			for each (var b : ConsoleButton in _buttons) b.visible = false;
		}

		private function rollOverHandler(event : MouseEvent) : void {
			for each (var b : ConsoleButton in _buttons) b.visible = _alignID != b.id;
		}

		private function resizeHandler(event : Event) : void {
			renderHandler(null);
		}

		/////////////////////////////////////////////////////////////////
		//  INVALIDATION
		/////////////////////////////////////////////////////////////////
		private function invalidate() : void {
			if (!stage) {
				_validateNextTime = true;
				return;
			}
			stage.addEventListener(Event.RENDER, renderHandler);
			stage.invalidate();
		}

		private function renderHandler(event : Event) : void {
			if (!stage) return;
			stage.removeEventListener(Event.RENDER, renderHandler);
			x = int(stage.stageWidth * _relatedX);
			y = int(stage.stageHeight * _relatedY);
			var w : int = stage.stageWidth * _relatedWidth;
			var h : int = stage.stageHeight * _relatedHight;
			with (graphics) {
				clear();
				beginFill(0xFFFFFF, 0.8);
				drawRect(0, 0, w, h);
				endFill();
			}
			for each (var b : ConsoleButton in _buttons) {
				b.x = b.relatedX * (w - ConsoleButton.BORDER);
				b.y = b.relatedY * (h - ConsoleButton.BORDER);
			}
			_input.width = _output.width = w - 2 * ConsoleButton.BORDER;
			_scrollV.height = _output.height = h - 2 * ConsoleButton.BORDER - _input.height;
			_scrollV.x = _output.width;
			addEventListener(Event.ENTER_FRAME, startCheckIn);
			_input.y = _output.y + _output.height;
		}

		private function startCheckIn(event : Event) : void {
			removeEventListener(Event.ENTER_FRAME, startCheckIn);
			checkScroll();
		}

		private function checkScroll() : void {
			if (!stage) return;
			_scrollV.visible = _output.maxScrollV > 1;
			if (!_scrollV.visible) return;
			_output.width = stage.stageWidth * _relatedWidth - 3 * ConsoleButton.BORDER;
			dispatchEvent(new SEvnt("_scrlH", _scrollV.value));
		}

		private function frameCount_enterFrameHandler(event : Event) : void {
			var s : String = "00000000" + ++_frames;
			_framePrefix = "f" + s.substr(s.length - 7) + "\t";
		}
	}
}

import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;

//////////////////////////////////////////////////////////////////////////////////////////
//
// ConsoleButton
//
//////////////////////////////////////////////////////////////////////////////////////////
internal class ConsoleButton extends Sprite {
	internal static const BORDER : Number = 14;
	private var _sp : Shape;
	public var id : uint;
	public var relatedX : Number;
	public var relatedY : Number;

	public function ConsoleButton() {
		useHandCursor = buttonMode = true;
		visible = false;
		with (graphics) {
			beginFill(0x444444);
			drawRect(0, 0, BORDER, BORDER);
			endFill();
		}
		_sp = new Shape();
		addChild(_sp);
		_sp.x = _sp.y = BORDER * .5;
		with (_sp.graphics) {
			lineStyle(2, 0xFFFFFF);
			moveTo(0, -4);
			lineTo(4, 0);
			lineTo(0, 4);
			moveTo(4, 0);
			lineTo(-4, 0);
			endFill();
		}
		addEventListener(MouseEvent.CLICK, clickHandler);
		addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
	}

	private function addedToStageHandler(event : Event) : void {
		stage.addEventListener("_cBC", selectedHandler, true);
	}

	private function selectedHandler(event : Event) : void {
		visible = event.target != this;
	}

	private function clickHandler(event : MouseEvent) : void {
		dispatchEvent(new Event("_cBC"));
	}

	public function set arrowRotation(value : Number) : void {
		_sp.rotation = value;
	}
}

//////////////////////////////////////////////////////////////////////////////////////////
//
// ConsoleScroll
//
//////////////////////////////////////////////////////////////////////////////////////////
internal class ConsoleScroll extends Sprite {
	private var _button : Sprite;
	private var _dy : Number;
	private var _height : Number = 0;
	private var _type : String;
	public var value : Number = 0;

	public function ConsoleScroll(type : String) {
		_type = type;
		super();
		_button = new Sprite();
		with (_button.graphics) {
			beginFill(0x979EA0);
			drawRect(0, 0, ConsoleButton.BORDER, ConsoleButton.BORDER);
			endFill();
		}
		buttonMode = useHandCursor = true;
		_button.addEventListener(MouseEvent.MOUSE_DOWN, button_mouseDownHandler);
		addChild(_button);
		addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
		addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
		height = 100;
	}

	private function mouseDownHandler(event : MouseEvent) : void {
		dispatchEvent(new SEvnt(_type, Math.min(Math.max(0, (mouseY - ConsoleButton.BORDER * .5) / (height - ConsoleButton.BORDER)), 1)));
		_button.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_DOWN));
	}

	private function addedToStageHandler(event : Event) : void {
		if (event.target != this) return;
		stage.addEventListener(_type, SEvntHandler, true);
	}

	private function SEvntHandler(event : SEvnt) : void {
		_button.y = (height - ConsoleButton.BORDER) * event.n;
		value = event.n;
	}

	private function button_mouseDownHandler(event : MouseEvent) : void {
		event.stopPropagation();
		_dy = _button.mouseY;
		stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
		stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
		stage.addEventListener(Event.MOUSE_LEAVE, mouseUpHandler);
	}

	private function mouseMoveHandler(event : MouseEvent) : void {
		var n : Number = Math.min(Math.max(0, (mouseY - _dy) / (height - ConsoleButton.BORDER)), 1);
		if (n != value) dispatchEvent(new SEvnt(_type, n)); 
		event.updateAfterEvent();
	}

	private function mouseUpHandler(event : Event) : void {
		stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
		stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
		stage.removeEventListener(Event.MOUSE_LEAVE, mouseUpHandler);
	}

	override public function set height(value : Number) : void {
		with (graphics) {
			clear();
			beginFill(0xFFFFFF);
			lineStyle(0, 0x7C8588);
			drawRect(0, 0, ConsoleButton.BORDER, value);
			endFill();
		}
		_height = value;
		_button.y = (_height - ConsoleButton.BORDER) * this.value;
	}

	override public function get height() : Number {
		return _height;
	}
}

//////////////////////////////////////////////////////////////////////////////////////////
//
// SEvnt
//
//////////////////////////////////////////////////////////////////////////////////////////
internal class SEvnt extends Event {
	public var n : Number;

	public function SEvnt(type : String, n : Number) {
		this.n = n;
		super(type, bubbles, cancelable);
	}

	override public function clone() : Event {
		return new SEvnt(type, n);
	}
}